Skip to content

第 3 章 Spring Boot的Web 应用支持 ​

本章重点:Web 开发是 Spring Boot 最核心的应用场景。本章从 Java Web 三大组件、Spring MVC 定制、文件上传到异常处理,全面讲解 Spring Boot 的 Web 开发能力。学完本章,你就能独立完成一个 Web 项目的基础搭建。

3.1 Java Web 三大组件注册 ​

3.1.1 三大组件简介 ​

Java Web 三大组件指的是 Servlet、Filter、Listener,这是 Java Web 开发的基础。

组件作用典型应用
Servlet处理 HTTP 请求,生成响应动态页面、接口处理
Filter拦截请求,在请求前后做处理编码过滤、登录校验、权限检查
Listener监听 Web 应用的事件统计在线人数、初始化资源

衔接知识:你们在 Java Web 课程里学过的 Servlet、Filter、Listener,在 Spring Boot 里依然能用。Spring Boot 内嵌了 Tomcat,所以这些原生 Web 组件照样可以注册使用。

为什么还要用原生组件?

虽然 Spring MVC 已经很强大了,但有时候还是需要用到原生 Web 组件:

  • 老项目迁移到 Spring Boot,原来的 Servlet/Filter 还能用
  • 某些第三方库依赖原生 Servlet API
  • 一些特殊场景(比如非常底层的过滤逻辑)用 Filter 更直接

Spring Boot 提供了三种注册方式,下面逐一讲解。

3.1.2 方式一:Spring Bean 注册(了解) ​

最简单的方式——直接把 Servlet、Filter、Listener 作为 Bean 注册到 Spring 容器,Spring Boot 会自动识别并注册到 Web 服务器。

示例代码 ​

自定义 Servlet:

java
public class FirstServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        System.out.println("hello FirstServlet");
        resp.getWriter().write("hello FirstServlet");
    }
}

自定义 Filter:

java
public class MyFilter implements Filter {
    @Override
    public void doFilter(ServletRequest requ, ServletResponse resp, FilterChain filterChain)
            throws IOException, ServletException {
        System.out.println("处理请求前的处理");
        filterChain.doFilter(requ, resp);
        System.out.println("处理请求后的处理");
    }
}

自定义 Listener:

java
public class MyListener implements ServletContextListener {
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        System.out.println("----Web应用初始化完成----");
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        System.out.println("----Web应用销毁之前----");
    }
}

配置类注册:

java
@Configuration
public class WebConfigure {

    @Bean
    public FirstServlet firstServlet() {
        return new FirstServlet();
    }

    @Bean
    public SecondServlet secondServlet() {
        return new SecondServlet();
    }

    @Bean
    public MyFilter myFilter() {
        return new MyFilter();
    }

    @Bean
    public MyListener myListener() {
        return new MyListener();
    }
}

映射规则 ​

  • Servlet:映射路径 = Bean名称 + /,比如 Bean 名叫 firstServlet,访问路径就是 /firstServlet/
  • Filter:默认映射路径是 /*(拦截所有请求)
  • Listener:自动注册,不需要映射路径

缺点 ​

  • Servlet 的映射路径由 Bean 名称决定,不够灵活
  • Filter 默认拦截所有请求,不能自定义拦截路径
  • 只有一个 Servlet 时,Bean 名称作为路径可能不符合预期

适用场景:简单场景、快速开发、对映射路径要求不高的情况。一般不推荐。

3.1.3 方式二:RegistrationBean 注册(推荐) ​

Spring Boot 提供了三个 RegistrationBean 来注册三大组件,可以灵活控制映射路径、顺序等参数。

组件对应的 RegistrationBean
ServletServletRegistrationBean
FilterFilterRegistrationBean
ListenerServletListenerRegistrationBean

示例代码 ​

java
@Configuration
public class WebConfigure {

    // 注册 Servlet
    @Bean
    public ServletRegistrationBean<FirstServlet> firstServlet() {
        return new ServletRegistrationBean<>(new FirstServlet(), "/first");
    }

    @Bean
    public ServletRegistrationBean<SecondServlet> secondServlet() {
        return new ServletRegistrationBean<>(new SecondServlet(), "/second");
    }

    // 注册 Filter
    @Bean
    public FilterRegistrationBean<MyFilter> myFilter() {
        FilterRegistrationBean<MyFilter> filterBean = new FilterRegistrationBean<>();
        filterBean.setFilter(new MyFilter());
        filterBean.setUrlPatterns(Arrays.asList("/first"));  // 只拦截 /first
        filterBean.setOrder(1);  // 过滤器执行顺序,数字越小越先执行
        return filterBean;
    }

    // 注册 Listener
    @Bean
    public ServletListenerRegistrationBean<MyListener> myListener() {
        return new ServletListenerRegistrationBean<>(new MyListener());
    }
}

常用配置 ​

ServletRegistrationBean 常用方法:

方法说明
setServlet()设置 Servlet 实例
addUrlMappings() / 构造方法设置映射路径
setLoadOnStartup()设置启动加载顺序
setInitParameters()设置初始化参数
setName()设置 Servlet 名称

FilterRegistrationBean 常用方法:

方法说明
setFilter()设置 Filter 实例
setUrlPatterns()设置拦截路径
setOrder()设置执行顺序(数字越小越先执行)
setInitParameters()设置初始化参数
setServletRegistrationBeans()指定拦截哪些 Servlet

企业最佳实践:推荐用 RegistrationBean 方式注册,灵活可控。尤其是 Filter,一定要指定拦截路径,不要默认拦截所有,避免影响性能。

3.1.4 方式三:注解扫描注册 ​

用 Servlet 3.0 提供的注解 @WebServlet、@WebFilter、@WebListener 标注组件,然后在启动类上加 @ServletComponentScan 扫描。

示例代码 ​

Servlet:

java
@WebServlet("/first")
public class FirstServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        resp.getWriter().write("hello FirstServlet");
    }
}

Filter:

java
@WebFilter("/first")
public class MyFilter implements Filter {
    @Override
    public void doFilter(ServletRequest requ, ServletResponse resp, FilterChain chain)
            throws IOException, ServletException {
        System.out.println("处理请求前");
        chain.doFilter(requ, resp);
        System.out.println("处理请求后");
    }
}

Listener:

java
@WebListener
public class MyListener implements ServletContextListener {
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        System.out.println("Web应用初始化");
    }
}

启动类添加扫描注解:

java
@SpringBootApplication
@ServletComponentScan  // 扫描 @WebServlet、@WebFilter、@WebListener
public class Chapter03Application {
    public static void main(String[] args) {
        SpringApplication.run(Chapter03Application.class, args);
    }
}

说明 ​

  • @ServletComponentScan 默认扫描启动类所在包及其子包
  • 可以指定扫描的包:@ServletComponentScan("com.itheima.web")
  • 不需要额外写配置类,代码更简洁

注意:Spring Boot 默认不会扫描 @WebServlet、@WebFilter、@WebListener 注解,必须加 @ServletComponentScan 才会生效。这是新手容易踩的坑——加了注解但忘了加扫描注解,组件不生效。

3.1.5 三种方式对比与选型 ​

对比项Spring Bean 注册RegistrationBean注解扫描
灵活性低高中
映射路径控制弱(Bean名决定)强(完全自定义)中(注解指定)
代码量少较多少
是否需要配置类是是否(启动类加注解)
顺序控制不支持支持(setOrder)支持(@Order)
初始化参数不支持支持支持(注解属性)
适用场景简单测试生产环境(推荐)中小项目

选型建议:

  • 生产环境推荐:RegistrationBean 方式,最灵活可控
  • 简单项目/快速开发:注解扫描方式,代码简洁
  • Spring Bean 方式:了解即可,实际项目用得少

小贴士:

  • 实际开发中,大部分场景用 Spring MVC 的 Controller 就够了,很少需要直接写 Servlet
  • Filter 用得相对多一些,比如做登录校验、跨域处理、日志记录等
  • Listener 用得最少,一般在应用启动时做初始化才会用到
  • 面试可能会问"Spring Boot 怎么注册 Servlet/Filter/Listener",三种方式都要能说出来

3.2 Spring MVC 自动配置与定制 ​

3.2.1 Spring MVC 自动配置特性 ​

Spring Boot 引入 spring-boot-starter-web 后,会自动配置 Spring MVC,开箱即用。自动配置主要做了这些事:

  1. 视图解析器:自动注册 ContentNegotiatingViewResolver 和 BeanNameViewResolver
  2. 静态资源支持:静态资源、WebJars 自动映射
  3. 转换器和格式化器:自动注册 Converter、Formatter
  4. 消息转换器:HttpMessageConverters,支持 JSON、XML 等
  5. 首页支持:自动映射 index.html
  6. 网站图标:自动加载 favicon.ico
  7. 数据绑定器:ConfigurableWebBindingInitializer 自动初始化

一句话总结:Spring Boot 把 Spring MVC 常用的配置都自动配好了,你直接用就行。

面试常问:Spring Boot 自动配置了 Spring MVC 的哪些东西? 答:视图解析器、静态资源映射、转换器/格式化器、消息转换器、首页支持、网站图标、数据绑定器等。

3.2.2 静态资源映射 ​

默认静态资源位置 ​

Spring Boot 默认会从以下 5 个位置查找静态资源,优先级从高到低:

优先级路径说明
1(最高)classpath:/META-INF/resources/WebJars 等第三方资源
2classpath:/resources/资源文件
3classpath:/static/静态资源(最常用)
4(最低)classpath:/public/公共资源

注意:这 5 个目录都是在 src/main/resources 下面的。一般项目都用 static 目录放静态资源。

访问方式 ​

把静态资源放到 static 目录下,直接通过 URL 访问:

src/main/resources/static/
├── css/
│   └── style.css
├── js/
│   └── app.js
├── images/
│   └── logo.png
└── index.html

访问地址:

  • http://localhost:8080/index.html
  • http://localhost:8080/css/style.css
  • http://localhost:8080/images/logo.png

不需要加 /static/ 前缀,直接访问文件名就行。

自定义静态资源映射 ​

默认的 5 个位置不够用?比如你想把文件存在 backend 目录下,或者存在磁盘的某个目录下,可以自定义映射。

方式一:配置类方式(推荐)

实现 WebMvcConfigurer 接口,重写 addResourceHandlers 方法:

java
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        // /backend/** 开头的请求,去 classpath:/backend/ 目录找资源
        registry.addResourceHandler("/backend/**")
                .addResourceLocations("classpath:/backend/");

        // 也可以映射到磁盘的绝对路径
        registry.addResourceHandler("/upload/**")
                .addResourceLocations("file:D:/upload/");
    }
}

方式二:配置文件方式

yaml
spring:
  mvc:
    static-path-pattern: /backend/**  # 静态资源的访问路径模式
  web:
    resources:
      static-locations: classpath:/backend/  # 静态资源存放的目录

两种方式的区别:

  • 配置文件方式:修改默认的静态资源路径,会覆盖默认的 5 个位置
  • 配置类方式:在默认的基础上新增映射,不影响默认的
  • 推荐用配置类方式,更灵活,可以加多个映射

WebJars 支持 ​

WebJars 是把前端资源(jQuery、Bootstrap 等)打成 JAR 包,通过 Maven 引入使用。

引入依赖:

xml
<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>jquery</artifactId>
    <version>3.6.0</version>
</dependency>

访问方式:

http://localhost:8080/webjars/jquery/3.6.0/jquery.js

Spring Boot 自动映射了 /webjars/** 到 classpath:/META-INF/resources/webjars/。

企业实践:现在前后端分离项目一般不用 WebJars,前端资源由前端项目管理。但传统的服务端渲染项目或者一些简单的后台管理系统可能会用到。

3.2.3 视图控制器(ViewController) ​

有些页面跳转不需要业务逻辑,只是简单的页面跳转。如果每个都写一个 Controller 方法,很麻烦。

传统写法:

java
@Controller
public class PageController {

    @GetMapping("/toLogin")
    public String toLogin() {
        return "login";  // 跳转到登录页
    }

    @GetMapping("/toRegister")
    public String toRegister() {
        return "register";  // 跳转到注册页
    }
}

用视图控制器简化:

java
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        // 访问 /backend/toLoginPage,跳转到 /backend/login.html
        registry.addViewController("/backend/toLoginPage")
                .setViewName("/backend/login.html");

        // 访问 /backend,跳转到 /backend/index.html
        registry.addViewController("/backend")
                .setViewName("/backend/index.html");
    }
}

这样就不用写 Controller 了,直接配置映射关系就行。

适用场景:没有业务逻辑的纯页面跳转。如果跳转前需要处理数据,还是得写 Controller。

3.2.4 拦截器(Interceptor) ​

拦截器是 Spring MVC 提供的,用于在请求处理前后做一些处理。和 Filter 类似,但更精细,可以拦截到具体的 Controller 方法。

拦截器 vs Filter ​

对比项Filter(过滤器)Interceptor(拦截器)
规范Servlet 规范Spring MVC 规范
拦截时机请求到达 Servlet 之前请求到达 Controller 之前
拦截粒度URL 级别方法级别(更细)
能否访问 Spring 容器不能(早期执行)能(Spring 管理)
能否获取方法信息不能能(HandlerMethod)
执行顺序Filter → Interceptor → Controller

自定义拦截器 ​

实现 HandlerInterceptor 接口,有三个方法:

java
public class LoginInterceptor implements HandlerInterceptor {

    // 请求处理之前调用(Controller 方法执行前)
    // 返回 true 放行,返回 false 拦截
    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) throws Exception {
        // 检查用户是否登录
        Object loginUser = request.getSession().getAttribute("loginUser");
        if (loginUser == null) {
            // 未登录,跳转到登录页
            request.getRequestDispatcher("/backend/login.html")
                   .forward(request, response);
            return false;  // 拦截
        }
        return true;  // 放行
    }

    // 请求处理之后调用(Controller 方法执行后,视图渲染前)
    @Override
    public void postHandle(HttpServletRequest request,
                           HttpServletResponse response,
                           Object handler,
                           ModelAndView modelAndView) throws Exception {
        // 可以修改 ModelAndView
    }

    // 整个请求完成后调用(视图渲染后)
    @Override
    public void afterCompletion(HttpServletRequest request,
                                HttpServletResponse response,
                                Object handler,
                                Exception ex) throws Exception {
        // 清理资源、记录日志等
    }
}

注册拦截器 ​

java
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .addPathPatterns("/**")          // 拦截所有请求
                .excludePathPatterns(            // 放行这些路径
                        "/backend/login.html",   // 登录页
                        "/backend/login",        // 登录接口
                        "/css/**",               // 静态资源
                        "/js/**",
                        "/images/**"
                );
    }
}

常用配置 ​

方法说明
addPathPatterns()添加拦截的路径
excludePathPatterns()排除不拦截的路径
order()拦截器执行顺序,数字越小越先执行

注意:拦截器默认会拦截静态资源!所以一定要把静态资源路径排除掉,否则 CSS、JS、图片都会被拦截,页面样式乱掉。

企业最佳实践:

  • 登录校验、权限验证用拦截器
  • 多个拦截器按顺序执行,注意 order 的设置
  • 拦截器里不要写复杂的业务逻辑,保持轻量
  • 前后端分离项目中,拦截器常用来校验 Token

3.2.5 WebMvcConfigurer 常用配置总结 ​

WebMvcConfigurer 接口提供了很多方法,可以定制 Spring MVC 的各种配置:

方法作用
addResourceHandlers()静态资源映射
addViewControllers()视图控制器(页面跳转)
addInterceptors()拦截器
configureMessageConverters()消息转换器(JSON/XML等)
addFormatters()格式化器(日期、数字等)
addCorsMappings()跨域配置
configureContentNegotiation()内容协商配置
extendMessageConverters()扩展消息转换器

记住常用的几个就行:静态资源、视图控制器、拦截器、跨域。其他的用到再查。

3.2.6 @EnableWebMvc 完全接管 MVC ​

默认情况下,Spring Boot 自动配置 + 你自定义的配置是并存的——自动配置保留,你的配置追加进去。

如果你加了 @EnableWebMvc 注解,Spring Boot 的 MVC 自动配置就会全部失效,完全由你自己管理。

java
@Configuration
@EnableWebMvc  // 关闭 Spring Boot 的 MVC 自动配置,完全自己控制
public class WebMvcConfig implements WebMvcConfigurer {
    // 所有配置都要自己写
}

重要提醒:

  • 一般不要加 @EnableWebMvc!加了之后所有自动配置都失效了,包括静态资源映射、消息转换器等,你得全部自己配
  • 只有在需要完全定制 MVC 配置的特殊场景才用
  • 新手最容易犯的错:看教程加了 @EnableWebMvc,结果静态资源访问不了、JSON 返回不对,排查半天
  • 记住:99% 的场景用 WebMvcConfigurer 就够了,不需要 @EnableWebMvc

3.3 文件上传与下载 ​

3.3.1 文件上传自动配置原理 ​

Spring Boot 对文件上传做了自动配置,由 MultipartAutoConfiguration 类实现:

  • 自动配置了 StandardServletMultipartResolver(基于 Servlet 3.0 的文件上传)
  • 不需要引入第三方 jar 包(比如 commons-fileupload)
  • 配置属性以 spring.servlet.multipart 开头

常用配置:

yaml
spring:
  servlet:
    multipart:
      enabled: true           # 启用文件上传(默认 true)
      max-file-size: 10MB     # 单个文件最大大小
      max-request-size: 50MB  # 整个请求最大大小(多文件上传时)
      file-size-threshold: 0  # 超过这个大小就写入临时文件(单位字节)
      location:               # 临时文件目录

注意:默认单个文件最大 1MB,整个请求最大 10MB。上传大文件一定要改配置,否则会报错。

3.3.2 单文件上传 ​

Controller 代码:

java
@RestController
public class FileController {

    @PostMapping("/upload")
    public Result upload(@RequestParam("file") MultipartFile file) {
        // 1. 判断文件是否为空
        if (file.isEmpty()) {
            return new Result(false, "请选择文件");
        }

        // 2. 获取原始文件名
        String originalFilename = file.getOriginalFilename();

        // 3. 生成新文件名(防止重名覆盖)
        String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
        String newFilename = UUID.randomUUID() + suffix;

        // 4. 指定上传目录
        String uploadDir = "src/main/resources/static/upload/";
        File dir = new File(uploadDir);
        if (!dir.exists()) {
            dir.mkdirs();  // 目录不存在就创建
        }

        // 5. 保存文件
        try {
            file.transferTo(new File(uploadDir + newFilename));
            return new Result(true, "上传成功", "/upload/" + newFilename);
        } catch (IOException e) {
            e.printStackTrace();
            return new Result(false, "上传失败");
        }
    }
}

MultipartFile 常用方法:

方法说明
isEmpty()文件是否为空
getOriginalFilename()获取原始文件名
getSize()获取文件大小(字节)
getContentType()获取文件类型
getBytes()获取文件字节数组
getInputStream()获取文件输入流
transferTo(File)保存文件到指定位置

3.3.3 多文件上传 ​

多文件上传用 MultipartFile[] 数组:

java
@PostMapping("/multiUpload")
public Result multiUpload(@RequestParam("files") MultipartFile[] files) {
    List<String> fileUrls = new ArrayList<>();

    for (MultipartFile file : files) {
        if (file.isEmpty()) {
            continue;
        }

        String originalFilename = file.getOriginalFilename();
        String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
        String newFilename = UUID.randomUUID() + suffix;

        String uploadDir = "src/main/resources/static/upload/";
        File dir = new File(uploadDir);
        if (!dir.exists()) {
            dir.mkdirs();
        }

        try {
            file.transferTo(new File(uploadDir + newFilename));
            fileUrls.add("/upload/" + newFilename);
        } catch (IOException e) {
            e.printStackTrace();
            return new Result(false, "上传失败");
        }
    }

    return new Result(true, "上传成功", fileUrls);
}

3.3.4 文件下载 ​

java
@GetMapping("/download")
public void download(@RequestParam String filename, HttpServletResponse response) {
    String filePath = "src/main/resources/static/upload/" + filename;
    File file = new File(filePath);

    if (!file.exists()) {
        response.setStatus(HttpServletResponse.SC_NOT_FOUND);
        return;
    }

    response.setContentType("application/octet-stream");
    response.setContentLength((int) file.length());

    try {
        // 设置下载文件名(处理中文文件名)
        String downloadName = URLEncoder.encode(filename, "UTF-8");
        response.setHeader("Content-Disposition",
                "attachment;filename=" + downloadName);

        // 写入响应
        Files.copy(file.toPath(), response.getOutputStream());
    } catch (IOException e) {
        e.printStackTrace();
    }
}

3.3.5 企业级实践 ​

1. 文件存储方案 ​

方案优点缺点适用场景
本地磁盘简单、免费单机存储、扩容难、丢失风险开发测试、小项目
NFS 共享存储多机共享性能一般、单点故障中小项目
对象存储(OSS/COS/七牛)海量存储、高可用、CDN加速收费生产环境(推荐)

企业现状:生产环境基本都用对象存储(阿里云 OSS、腾讯云 COS、七牛云等),不会存在本地。本地存储只在开发测试用。

2. 文件命名策略 ​

  • 不要用原始文件名:可能重名覆盖,可能有特殊字符
  • 推荐:UUID + 后缀,或者日期路径 + UUID
  • 示例:2024/01/15/uuid.jpg,按日期分目录,便于管理

3. 安全校验 ​

  • 校验文件类型:不能只看后缀名,要校验文件头(Magic Number)
  • 校验文件大小:防止超大文件攻击
  • 文件名处理:过滤特殊字符,防止路径遍历攻击
  • 上传目录隔离:上传目录不能在可执行目录下

4. 其他注意事项 ​

  • 上传的文件不要放在 resources 目录下,打包后就写不进去了
  • 生产环境要配置独立的上传目录,最好在项目外面
  • 大文件上传要考虑分片上传
  • 要有文件清理策略,防止磁盘占满

小贴士:

  • 面试可能会问"文件上传怎么实现的",要能说出 MultipartFile、配置、注意事项
  • 生产环境用对象存储,这是标配,提前了解一下阿里云 OSS 的使用
  • 文件上传是安全漏洞的高发区,一定要做校验,不能什么文件都让传

3.4 异常处理 ​

3.4.1 Spring Boot 默认异常处理机制 ​

Spring Boot 提供了默认的异常处理机制,由 ErrorMvcAutoConfiguration 自动配置类实现。

四大核心组件 ​

组件作用
ErrorPageCustomizer错误页面定制,发生异常后转发到 /error
BasicErrorController错误控制器,处理 /error 请求
DefaultErrorViewResolver默认错误视图解析器
DefaultErrorAttributes错误属性处理,封装错误信息

异常处理流程 ​

  1. 程序抛出异常
  2. ErrorPageCustomizer 将请求转发到 /error
  3. BasicErrorController 处理 /error 请求
  4. 根据请求类型返回不同结果:
    • 浏览器请求(Accept: text/html)→ 返回错误页面(HTML)
    • 其他请求(如 Ajax)→ 返回 JSON 数据
  5. DefaultErrorViewResolver 解析错误视图
  6. DefaultErrorAttributes 封装错误信息

默认错误信息 ​

默认返回的错误信息包含这些字段:

字段说明
timestamp时间戳
statusHTTP 状态码
error错误描述
exception异常类名
message错误消息
trace异常栈信息
path请求路径

了解原理即可:实际开发中我们一般不用默认的,都会自定义异常处理。

3.4.2 自定义错误页面 ​

如果只是想自定义错误页面的样式,很简单:

在 templates/error/ 目录下创建对应状态码的 HTML 页面:

src/main/resources/templates/error/
├── 404.html    ← 404 错误页面
├── 4xx.html    ← 4xx 系列错误页面(404 没有的话用这个)
├── 500.html    ← 500 错误页面
└── 5xx.html    ← 5xx 系列错误页面

或者在静态资源目录的 error/ 下:

src/main/resources/static/error/
└── 404.html

Spring Boot 会自动查找这些页面,精确匹配优先(404.html 优先于 4xx.html)。

3.4.3 全局异常处理(推荐) ​

用 @ControllerAdvice + @ExceptionHandler 做全局异常处理,这是企业里最常用的方式。

基本用法 ​

java
@ControllerAdvice
public class MyExceptionHandler {

    // 处理自定义业务异常
    @ExceptionHandler(MyException.class)
    @ResponseBody
    public Result handleMyException(MyException e) {
        e.printStackTrace();
        return new Result(false, e.getMessage());
    }

    // 处理系统异常
    @ExceptionHandler(Exception.class)
    @ResponseBody
    public Result handleException(Exception e) {
        e.printStackTrace();
        return new Result(false, "系统繁忙,请稍后重试");
    }
}

如果是 @RestControllerAdvice,就不用加 @ResponseBody 了,默认都返回 JSON:

java
@RestControllerAdvice  // = @ControllerAdvice + @ResponseBody
public class GlobalExceptionHandler {

    @ExceptionHandler(MyException.class)
    public Result handleMyException(MyException e) {
        return new Result(false, e.getMessage());
    }

    @ExceptionHandler(Exception.class)
    public Result handleException(Exception e) {
        e.printStackTrace();
        return new Result(false, "系统错误");
    }
}

工作原理 ​

  • @ControllerAdvice:控制器增强,全局拦截 Controller 的异常
  • @ExceptionHandler:指定处理哪种异常
  • 异常匹配规则:优先匹配精确的异常类型,没有的话匹配父类
  • 可以有多个 @ExceptionHandler 方法,分别处理不同异常

3.4.4 自定义业务异常 ​

企业开发中,通常会定义自己的业务异常类,用来区分业务异常和系统异常。

自定义异常类:

java
public class MyException extends RuntimeException {

    private String code;  // 错误码

    public MyException(String message) {
        super(message);
    }

    public MyException(String code, String message) {
        super(message);
        this.code = code;
    }

    public String getCode() {
        return code;
    }
}

错误码枚举(更规范的写法):

java
public enum ErrorEnum {
    // 400 系列
    BAD_REQUEST("400", "请求的数据格式不符!"),
    UNAUTHORIZED("401", "登录凭证过期!"),
    FORBIDDEN("403", "抱歉,你无权限访问!"),
    NOT_FOUND("404", "请求的资源找不到!"),

    // 500 系列
    INTERNAL_SERVER_ERROR("500", "服务器内部错误!"),
    SERVICE_UNAVAILABLE("503", "服务器正忙,请稍后再试!"),

    // 业务异常
    USER_NOT_EXIST("10001", "用户不存在"),
    PASSWORD_ERROR("10002", "密码错误"),
    PARAM_ERROR("10003", "参数错误");

    private final String code;
    private final String msg;

    ErrorEnum(String code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public String getCode() { return code; }
    public String getMsg() { return msg; }
}

使用方式:

java
@RestController
@RequestMapping("/dish")
public class DishController {

    @GetMapping("/del/{id}")
    public Result delDish(@PathVariable String id) {
        Integer dishId = Integer.valueOf(id);
        if (dishId < 0) {
            // 抛出业务异常
            throw new MyException(ErrorEnum.PARAM_ERROR.getMsg());
        }
        return new Result(true, "删除成功");
    }
}

3.4.5 统一返回结果与异常处理结合 ​

企业项目中,通常会定义统一的返回结果格式,异常处理也返回统一格式。

统一返回结果类:

java
@Data
public class Result<T> implements Serializable {
    private boolean flag;    // 是否成功
    private String message;  // 提示信息
    private T data;          // 返回数据

    public Result(boolean flag, String message) {
        this.flag = flag;
        this.message = message;
    }

    public Result(boolean flag, String message, T data) {
        this.flag = flag;
        this.message = message;
        this.data = data;
    }

    // 快捷方法
    public static <T> Result<T> success(String message, T data) {
        return new Result<>(true, message, data);
    }

    public static <T> Result<T> success(T data) {
        return new Result<>(true, "操作成功", data);
    }

    public static <T> Result<T> error(String message) {
        return new Result<>(false, message);
    }
}

全局异常处理器:

java
@RestControllerAdvice
public class GlobalExceptionHandler {

    // 处理业务异常
    @ExceptionHandler(MyException.class)
    public Result handleBusinessException(MyException e) {
        // 业务异常,返回具体的错误信息
        return Result.error(e.getMessage());
    }

    // 处理参数校验异常
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result handleValidationException(MethodArgumentNotValidException e) {
        String message = e.getBindingResult().getFieldError().getDefaultMessage();
        return Result.error(message);
    }

    // 处理系统异常
    @ExceptionHandler(Exception.class)
    public Result handleException(Exception e) {
        // 系统异常,打日志,返回通用提示
        e.printStackTrace();  // 生产环境用日志框架
        return Result.error("系统繁忙,请稍后重试");
    }
}

3.4.6 企业级异常处理最佳实践 ​

  1. 分层处理异常:

    • 业务异常(自定义异常):返回具体错误信息,用户能看懂
    • 系统异常(Exception):返回通用提示,不暴露内部细节
    • 参数校验异常:返回校验失败的具体字段和原因
  2. 异常信息要打日志:

    • 业务异常可以不打(预期内的)
    • 系统异常一定要打 error 级别日志,方便排查
  3. 不要捕获异常后不处理:

    • 最忌讳 catch (Exception e) {},吞掉异常不处理
    • 要么处理,要么往上抛,不要静默失败
  4. 异常粒度要合适:

    • 不要什么都抛 RuntimeException
    • 不同业务场景定义不同的异常类型,便于区分处理
  5. 不要用异常控制业务流程:

    • 异常是用来处理异常情况的,不是用来做正常流程控制的
    • 比如"用户不存在"这种预期内的情况,可以返回错误码,不一定非要抛异常

面试常问:Spring Boot 怎么做全局异常处理? 答:用 @RestControllerAdvice + @ExceptionHandler,定义全局异常处理类,分别处理不同类型的异常,返回统一格式的结果。


3.5 Web 开发进阶(企业级扩展) ​

3.5.1 跨域处理(CORS) ​

什么是跨域 ​

浏览器的同源策略:协议、域名、端口都相同才叫同源,不同源的请求会被浏览器拦截。

跨域是前后端分离项目必遇到的问题——前端在 http://localhost:8080,后端在 http://localhost:9090,端口不同,就是跨域。

Spring Boot 解决跨域 ​

方式一:全局配置(推荐)

java
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")                    // 允许所有路径
                .allowedOriginPatterns("*")           // 允许所有来源
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")  // 允许的方法
                .allowedHeaders("*")                  // 允许的请求头
                .allowCredentials(true)               // 允许携带 Cookie
                .maxAge(3600);                        // 预检请求缓存时间(秒)
    }
}

方式二:注解方式

java
@RestController
@CrossOrigin(origins = "*")  // 整个 Controller 都允许跨域
public class UserController {

    @GetMapping("/list")
    @CrossOrigin(origins = "http://localhost:8080")  // 单个方法也可以加
    public Result list() {
        return Result.success();
    }
}

方式三:Filter 方式

java
@Configuration
public class CorsConfig {

    @Bean
    public CorsFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        config.addAllowedOriginPattern("*");
        config.addAllowedHeader("*");
        config.addAllowedMethod("*");
        config.setAllowCredentials(true);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    }
}

注意:allowedOrigins("*") 和 allowCredentials(true) 不能同时用(Spring Boot 2.4+),要用 allowedOriginPatterns("*") 代替。

3.5.2 数据校验(JSR-303) ​

第 1 章简单提过,这里再详细讲一下。后端接口一定要校验参数,不能信任前端传过来的任何数据。

常用校验注解:

注解说明
@NotNull不能为 null
@NotBlank不能为 null,也不能是空字符串(去掉空格后)
@NotEmpty不能为 null,集合/数组不能为空
@Min数字最小值
@Max数字最大值
@Size字符串/集合大小范围
@Email邮箱格式
@Pattern正则表达式匹配
@Valid级联校验(嵌套对象)

示例:

java
@Data
public class UserDTO {

    @NotBlank(message = "用户名不能为空")
    @Size(min = 3, max = 20, message = "用户名长度3-20位")
    private String username;

    @NotBlank(message = "密码不能为空")
    @Size(min = 6, max = 20, message = "密码长度6-20位")
    private String password;

    @Email(message = "邮箱格式不正确")
    private String email;

    @Min(value = 0, message = "年龄不能小于0")
    @Max(value = 150, message = "年龄不能大于150")
    private Integer age;
}

Controller 中使用:

java
@PostMapping("/add")
public Result add(@Valid @RequestBody UserDTO userDTO) {
    // 参数校验通过才会进入方法
    return Result.success();
}

校验不通过会抛出 MethodArgumentNotValidException,在全局异常处理中捕获:

java
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result handleValidationException(MethodArgumentNotValidException e) {
    String message = e.getBindingResult().getFieldError().getDefaultMessage();
    return Result.error(message);
}

重要原则:前端校验是为了用户体验,后端校验是为了安全。后端校验绝对不能少!

3.5.3 统一返回结果 ​

前面已经提到过,这里再强调一下规范。

标准结构:

json
{
  "code": 200,
  "message": "操作成功",
  "data": {}
}

常见状态码设计:

状态码含义
200成功
400请求参数错误
401未登录 / Token 过期
403无权限
404资源不存在
500服务器内部错误
10001业务异常(用户不存在等)

企业实践:

  • 前后端分离项目,统一返回 JSON 格式
  • 状态码要和前端约定好,文档化
  • 成功和失败的结构要一致,前端好处理

3.5.4 常见 Web 问题排查 ​

问题可能原因排查方法
静态资源 404路径不对、目录不对、被拦截器拦了检查文件位置、访问路径、拦截器配置
跨域报错后端没配跨域、配置不对检查 CORS 配置、看浏览器控制台错误
文件上传失败大小超限、路径不存在、权限问题检查 multipart 配置、上传目录
拦截器不生效没注册、路径不匹配、顺序问题检查 addInterceptors 配置
全局异常不生效没加 @RestControllerAdvice、包没扫描到检查注解、包扫描范围
配置不生效优先级问题、配置写错了检查配置文件位置、拼写、缩进

3.6 本章小结 ​

核心知识点回顾:

  1. Java Web 三大组件注册:

    • 三种方式:Spring Bean、RegistrationBean(推荐)、注解扫描
    • Servlet、Filter、Listener 各自的作用
    • 实际开发中用得不多,但要知道怎么注册
  2. Spring MVC 定制:

    • 自动配置了哪些特性
    • 静态资源映射(默认位置、自定义映射)
    • 视图控制器(无业务逻辑的页面跳转)
    • 拦截器(登录校验、权限验证)
    • WebMvcConfigurer 常用配置
    • 不要随便加 @EnableWebMvc
  3. 文件上传:

    • MultipartFile 的使用
    • 单文件、多文件上传
    • 上传配置(大小限制)
    • 企业级实践(对象存储、安全校验)
  4. 异常处理:

    • 默认异常处理原理(四大组件)
    • 全局异常处理(@RestControllerAdvice + @ExceptionHandler)
    • 自定义业务异常
    • 统一返回结果
    • 异常处理最佳实践
  5. Web 开发进阶:

    • 跨域处理(CORS)
    • 数据校验(JSR-303)
    • 统一返回结果规范

面试高频考点:

  • Spring Boot 怎么注册 Servlet/Filter/Listener
  • Spring MVC 自动配置了哪些内容
  • 静态资源映射的默认位置
  • 拦截器的使用和原理
  • 全局异常处理怎么实现
  • 跨域怎么解决

给实习生的建议:

  • 这一章是 Web 开发的基础,一定要动手敲代码练
  • 重点掌握:拦截器、文件上传、全局异常处理、跨域,这些是工作中天天用的
  • 统一返回结果、参数校验、异常处理,这三个是项目规范,进公司第一天就要遵守
  • 多思考为什么这么设计,而不只是记住怎么写

3.7 动手练习作业 ​

基础必做题(全员必做) ​

  1. 三大组件练习
    • 自定义一个 Filter,实现所有请求的访问日志打印(请求路径、时间)
    • 用 FilterRegistrationBean 注册,设置执行顺序,排除静态资源路径
  2. 拦截器练习
    • 编写登录拦截器,拦截 /admin/** 路径的请求
    • 未登录跳转到登录页,已登录放行
    • 排除登录页、静态资源路径
    • 测试验证:直接访问后台路径被拦截,登录后可正常访问
  3. 文件上传功能
    • 实现单文件上传接口,限制图片类型,大小 5MB 以内
    • 文件保存到项目外部目录,UUID 重命名
    • 返回文件访问路径,配置静态资源映射可直接浏览器访问
    • 测试上传不同大小、不同类型文件,验证校验效果
  4. 全局异常处理
    • 编写全局异常处理器 GlobalExceptionHandler
    • 自定义业务异常类和错误码枚举
    • 分别处理业务异常、参数校验异常、系统异常
    • 所有异常统一返回 Result 格式
  5. 参数校验练习
    • 编写用户注册 DTO,添加用户名、密码、邮箱、年龄校验
    • Controller 方法添加 @Valid 触发校验
    • 测试非法参数,验证全局异常能否正确返回校验信息

进阶选做题(学有余力完成) ​

  1. 多文件上传:实现批量文件上传,返回所有文件的访问路径列表
  2. 文件下载:实现文件下载接口,支持中文文件名,测试下载正常
  3. 多拦截器:再添加一个权限拦截器,验证管理员角色,设置执行顺序,登录拦截器先执行,权限拦截器后执行
  4. 异常日志记录:集成 SLF4J 日志,系统异常记录 error 级别日志,包含异常栈信息

思考题 ​

  1. Filter 和拦截器有什么区别?分别在什么场景使用?
  2. 为什么加了 @EnableWebMvc 之后静态资源访问不了了?
  3. 文件上传有哪些安全风险?应该怎么防护?
  4. 全局异常处理的原理是什么?为什么能拦截所有 Controller 的异常?
  5. 前后端分离项目为什么会有跨域问题?有哪些解决方案?

基于 Vite 强力驱动 | 纯静态轻量托管